﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Media3D;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows;

namespace Glissando
{
    public class PianoModel3D : UIElement3D
    {
        // The Model property for the piano
        private static readonly DependencyProperty ModelProperty =
            DependencyProperty.Register("Model",
                                        typeof(Model3D),
                                        typeof(PianoModel3D),
                                        new PropertyMetadata(ModelPropertyChanged));

        private static void ModelPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            PianoModel3D s = (PianoModel3D)d;
            s.Visual3DModel = s.Model;
        }

        public Model3D Model
        {
            get
            {
                return (Model3D)GetValue(ModelProperty);
            }

            set
            {
                SetValue(ModelProperty, value);
            }
        }

        protected override void OnUpdateModel()
        {
            Model = CreatePianoModel();
        }

        public bool ColoredKeys
        {
            get { return (bool)GetValue(ColoredKeysProperty); }
            set { SetValue(ColoredKeysProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ChangeKeyColor.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ColoredKeysProperty =
            DependencyProperty.Register("ColoredKeys", typeof(bool), typeof(PianoModel3D), new PropertyMetadata(false));


        Material keyDownMaterial1;
        Material keyDownMaterial2;

        List<GeometryModel3D> keys = new List<GeometryModel3D>();
        Dictionary<GeometryModel3D, int> keyToNote = new Dictionary<GeometryModel3D, int>();
        List<Material> originalKeyMaterial = new List<Material>();

        #region Public Methods
        public Point3D GetKeyPosition(int p)
        {
            return new Point3D(FindKeyPos(p), -whiteKeyLength / 2, 0);
        }

        public void ReleaseAllKeys()
        {
            for (int i = 21; i <= 108; i++)
                ReleaseKey(i);
        }

        public void ReleaseKey(int keyNumber)
        {
            int i = keyNumber - 21;
            if (i < 0 || i >= keys.Count) return;
            GeometryModel3D key = keys[i];
            key.Material = key.BackMaterial = originalKeyMaterial[i];
            //key.Transform = null;

            RotateTransform3D rt = key.Transform as RotateTransform3D;
            if (rt != null)
            {
                AxisAngleRotation3D rot = rt.Rotation as AxisAngleRotation3D;
                double speed = 80;
                DoubleAnimation a = new DoubleAnimation(0, new Duration(TimeSpan.FromMilliseconds(speed)));
                a.AccelerationRatio = 0.3;
                a.DecelerationRatio = 0.5;
                rot.BeginAnimation(AxisAngleRotation3D.AngleProperty, a);
            }
        }

        public void PressKey(int channel, int keyNumber, int force)
        {
            int i = keyNumber - 21;
            if (i < 0 || i >= keys.Count) return;
            GeometryModel3D key = keys[i];
            AxisAngleRotation3D rot = new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0);
            key.Transform = new RotateTransform3D(rot, new Point3D(0, rotationPosition, 0));
            if (ColoredKeys)
                key.Material = key.BackMaterial = channel % 2 == 1 ? keyDownMaterial1 : keyDownMaterial2;

            double speed = 20 * (1 + (128 - force) * 0.03);
            DoubleAnimation a = new DoubleAnimation(3, new Duration(TimeSpan.FromMilliseconds(speed)));
            a.AccelerationRatio = 0.3;
            a.DecelerationRatio = 0.5;
            rot.BeginAnimation(AxisAngleRotation3D.AngleProperty, a);
        }
        #endregion

        #region Create piano model
        private Model3DGroup CreatePianoModel()
        {
            Model3DGroup group = new Model3DGroup();
            keys.Clear();

            for (int i = 21; i <= 108; i++)
            {
                GeometryModel3D key = AddKey(i, group);
                keys.Add(key);
                keyToNote.Add(key, i);
            }

            GeometryModel3D redStrip = CreateRedStrip();
            group.Children.Add(redStrip);

            group.Children.Add(CreateBack());
            group.Children.Add(CreateSide(20, -20));
            group.Children.Add(CreateSide(109, 20));

            originalKeyMaterial.Clear();
            foreach (GeometryModel3D key in keys)
                originalKeyMaterial.Add(key.Material);

            keyDownMaterial1 = CreateMaterial(Colors.Red);
            keyDownMaterial2 = CreateMaterial(Colors.Green);

            return group;
        }

        // distance between keys
        double keyDistance = 2;
        // where the keys rotate
        double rotationPosition = 100;

        // white keys
        double whiteKeyWidth = 22;
        double whiteKeyLength = 150;
        double whiteKeyHeight = 21;
        double whiteLayerThickness = 0.8;       // thickness of the white layer
        double whiteFrontOverhang = 0.5;        // overhang in the front       

        // black keys
        double blackKeyWidth = 11.5;
        double blackKeyWidthTop = 9.5;
        double blackKeylength = 100;
        double blackKeyHeight = 11;
        double blackFront = 8;

        // red felt strip
        double redStripWidth = 2;
        double redStripHeight = 1;

        private GeometryModel3D CreateRedStrip()
        {
            double x0 = FindKeyPos(20);
            double x1 = FindKeyPos(109);
            double y0 = 0;
            double y1 = redStripWidth;
            double z1 = whiteKeyHeight;
            double z0 = whiteKeyHeight + redStripHeight;
            return CreateCube(x0, x1, y0, y1, z0, z1, Colors.Red);
        }

        private GeometryModel3D CreateBack()
        {
            double x0 = FindKeyPos(20) - 20;
            double x1 = FindKeyPos(109) + 20;
            double y0 = redStripWidth;
            double y1 = y0 + 20;
            double z0 = 0;
            double z1 = z0 + 60;
            return CreateCube(x0, x1, y0, y1, z0, z1, Colors.Black);
        }

        private GeometryModel3D CreateSide(int key, double width)
        {
            double x0 = FindKeyPos(key);
            double x1 = x0 + width;
            double y0 = -whiteKeyLength;
            double y1 = redStripWidth + 20;
            double z0 = 0;
            double z1 = z0 + 40;
            return CreateCube(x0, x1, y0, y1, z0, z1, Colors.Black);
        }

        private GeometryModel3D CreateCube(double x0, double x1, double y0, double y1, double z0, double z1, Color c)
        {   //   E  G
            //    AC
            //    BD 
            //   F  H
            Material material = CreateMaterial(c);
            MeshGeometry3D mesh = new MeshGeometry3D();

            Point3D A = new Point3D(x0, y0, z0);
            Point3D B = new Point3D(x0, y1, z0);
            Point3D C = new Point3D(x1, y0, z0);
            Point3D D = new Point3D(x1, y1, z0);
            Point3D E = new Point3D(x0, y0, z1);
            Point3D F = new Point3D(x0, y1, z1);
            Point3D G = new Point3D(x1, y0, z1);
            Point3D H = new Point3D(x1, y1, z1);
            int n = 8;
            int m = 8;
            AddMesh(mesh, A, B, D, C, n, m);
            AddMesh(mesh, F, H, D, B, n, m);
            AddMesh(mesh, H, G, C, D, n, m);
            AddMesh(mesh, A, C, G, E, n, m);
            AddMesh(mesh, E, F, B, A, n, m);
            AddMesh(mesh, F, E, G, H, n, m);

            GeometryModel3D key = new GeometryModel3D(mesh, material);
            key.BackMaterial = material;
            return key;
        }



        int FindOctave(int i)
        {
            return i / 12 - 2;
        }

        int FindNumber(int i)
        {
            int[] k = { 0, 10, 1, 11, 2, 3, 12, 4, 13, 5, 14, 6 };
            return k[i % 12];
        }

        double FindKeyPos(int i)
        {
            int octave = FindOctave(i);
            int j = FindNumber(i);
            if (j < 10)
                return GetWhiteKeyLeftPosition(octave, j) + whiteKeyWidth / 2;
            else
                return GetBlackKeyLeftPosition(octave, j - 10) + blackKeyWidth / 2;
        }

        double GetWhiteKeyLeftPosition(int octave, int j)
        {
            return (octave * 7 + j) * (whiteKeyWidth + keyDistance);
        }

        double GetBlackKeyLeftPosition(int octave, int i)
        {
            int i0 = octave * 7 + i;
            if (i > 1) i0 += 1;
            double xx0 = i0 * (whiteKeyWidth + keyDistance);
            double[] f = { 0.75, 0.25, 0.75, 0.5, 0.25 };
            double a0 = whiteKeyWidth - blackKeyWidth * f[i];
            double x0 = xx0 + a0 + keyDistance / 2;
            return x0;
        }

        GeometryModel3D AddKey(int i, Model3DGroup group)
        {
            int octave = FindOctave(i);
            int j = FindNumber(i);
            if (j < 10)
                return AddWhiteKey(j, octave, group);
            else
                return AddBlackKey(j - 10, octave, group);
        }

        private GeometryModel3D AddWhiteKey(int i, int octave, Model3DGroup group)
        {
            /*
                        Top view

                     x0 x2  x3 x1
                        |----|    y0         6  5
                        |    |  
                        |    |  
                        |    |  
                      |-|    |-|  y1    11/3 7  4  2/10
                      |        |
                      |        |          
                      |        |
                      +--------+  y2              
                      +--------+  y3    8             9
                      +--------+  y4       0       1

                      Front view

                      +--------+   z1
                      +--------+   z2
                      |        |
                      +--------+   z0

                      Side view

                      +---------------------+   z1
                      +---------------------+   z2
                      |        |       |  |
                      +----------------+--+   z0
                      y0       y1     y2 y3 y4    
             */

            // todo - wood in the core...

            double x0 = GetWhiteKeyLeftPosition(octave, i);
            double x1 = x0 + whiteKeyWidth;
            double y0 = 0;
            double y1 = -blackKeylength;
            double y2 = -whiteKeyLength;
            double y3 = y2 - whiteLayerThickness;
            double y4 = y3 - whiteFrontOverhang;
            double z0 = 0;
            double z1 = whiteKeyHeight;
            double z2 = whiteKeyHeight - whiteLayerThickness;

            double[] f1 = { 0.75, 0.25, 0, 0.75, 0.5, 0.25, 0 };
            double[] f0 = { 0, 0.25, 0.75, 0, 0.25, 0.5, 0.75 };
            double a0 = blackKeyWidth * f0[i];
            double a1 = blackKeyWidth * f1[i];
            if (octave == -1) a0 = 0;
            if (octave == 7) a1 = 0;
            double x2 = x0 + a0;
            double x3 = x1 - a1;

            Material material = CreateMaterial(Colors.White);
            MeshGeometry3D mesh = new MeshGeometry3D();
            Point3D[] pts = new Point3D[12];
            pts[0] = new Point3D(x0, y4, z1);
            pts[1] = new Point3D(x1, y4, z1);
            pts[2] = new Point3D(x1, y1, z1);
            pts[3] = new Point3D(x0, y1, z1);
            pts[4] = new Point3D(x3, y1, z1);
            pts[5] = new Point3D(x3, y0, z1);
            pts[6] = new Point3D(x2, y0, z1);
            pts[7] = new Point3D(x2, y1, z1);
            pts[8] = new Point3D(x0, y3, z2);
            pts[9] = new Point3D(x1, y3, z2);
            pts[10] = new Point3D(x1, y1, z2);
            pts[11] = new Point3D(x0, y1, z2);

            AddQuad(mesh, pts[0], pts[1], pts[2], pts[3]);
            AddQuad(mesh, pts[4], pts[5], pts[6], pts[7]);
            
            AddQuadProjectZ(mesh, pts[0], pts[1], pts[2], pts[3], z2);
            AddQuadProjectZ(mesh, pts[8], pts[9], pts[10], pts[11], z0);

            AddQuadProjectZ(mesh, pts[4], pts[5], pts[6], pts[7], z0);
            
            AddQuadProjectZ(mesh, pts[0], pts[1], z2); // front
            AddQuadProjectZ(mesh, pts[1], pts[2], z2); // right side
            AddQuadProjectZ(mesh, pts[8], pts[9], z0);
            AddQuadProjectZ(mesh, pts[9], pts[10], z0);

            AddQuadProjectZ(mesh, pts[2], pts[4], z0);
            AddQuadProjectZ(mesh, pts[4], pts[5], z0);
            AddQuadProjectZ(mesh, pts[5], pts[6], z0);
            AddQuadProjectZ(mesh, pts[6], pts[7], z0);
            AddQuadProjectZ(mesh, pts[7], pts[3], z0);
            AddQuadProjectZ(mesh, pts[3], pts[0], z2);
            AddQuadProjectZ(mesh, pts[11], pts[8], z0);

            GeometryModel3D key = new GeometryModel3D(mesh, material);
            key.BackMaterial = material;
            group.Children.Add(key);
            return key;
        }

        private GeometryModel3D AddBlackKey(int i, int octave, Model3DGroup group)
        {
            /*
                        Top view

                     x0 x1  x2 x3
                      |-+----+-|  y0       8 4   3 7
                      | |    | |
                      | |    | |
                      | |    | |
                      | |    | |
                      | |    | |
                      |-+----+-|  y1       9 5   2 6
                      |/      \|
                      +--------+  y2       0       1

                      Front view

                        +----+     z2
                       /      \
                      +--------+   z1
                      |        |
                      |        |
                      +--------+   z0

             */
            double x0 = GetBlackKeyLeftPosition(octave, i);
            double x3 = x0 + blackKeyWidth;
            double x1 = x0 + (blackKeyWidth - blackKeyWidthTop) / 2;
            double x2 = x3 - (blackKeyWidth - blackKeyWidthTop) / 2;
            double y0 = 0;
            double y1 = -blackKeylength + blackFront;
            double y2 = -blackKeylength + keyDistance / 2;

            double z0 = 0;
            double z1 = whiteKeyHeight;
            double z2 = whiteKeyHeight + blackKeyHeight;

            Material material = CreateMaterial(Colors.Black);
            MeshGeometry3D mesh = new MeshGeometry3D();
            Vector3D normal = new Vector3D(0, 0, 1);

            Point3D[] pts = new Point3D[10];
            pts[0] = new Point3D(x0, y2, z1);
            pts[1] = new Point3D(x3, y2, z1);
            pts[2] = new Point3D(x2, y1, z2);
            pts[3] = new Point3D(x2, y0, z2);
            pts[4] = new Point3D(x1, y0, z2);
            pts[5] = new Point3D(x1, y1, z2);
            pts[6] = new Point3D(x3, y1, z1);
            pts[7] = new Point3D(x3, y0, z1);
            pts[8] = new Point3D(x0, y0, z1);
            pts[9] = new Point3D(x0, y1, z1);

            AddQuad(mesh, pts[0], pts[1], pts[2], pts[5]); // front slope
            AddQuad(mesh, pts[2], pts[3], pts[4], pts[5]); // top
            AddQuadProjectZ(mesh, pts[0], pts[1], pts[7], pts[8], z0); // bottom
            AddQuad(mesh, pts[2], pts[3], pts[7], pts[6]); // right side
            AddQuad(mesh, pts[8], pts[9], pts[5], pts[4]); // left side
            AddTriangle(mesh, pts[1], pts[6], pts[2]); // right side triangle
            AddTriangle(mesh, pts[5], pts[9], pts[0]);         // left side triangle
            AddQuad(mesh, pts[3], pts[7], pts[8], pts[4]); // back

            AddQuadProjectZ(mesh, pts[0], pts[1], z0);
            AddQuadProjectZ(mesh, pts[1], pts[7], z0);
            AddQuadProjectZ(mesh, pts[7], pts[8], z0);
            AddQuadProjectZ(mesh, pts[8], pts[0], z0);
            AddQuadProjectZ(mesh, pts[4], pts[5], z0);
            AddQuadProjectZ(mesh, pts[5], pts[0], z0);

            GeometryModel3D key = new GeometryModel3D(mesh, material);
            key.BackMaterial = material;
            group.Children.Add(key);
            return key;
        }

        Material CreateMaterial(Color c)
        {
            MaterialGroup mg = new MaterialGroup();
            mg.Children.Add(new DiffuseMaterial(new SolidColorBrush(c)));
            mg.Children.Add(new SpecularMaterial(Brushes.White, 50));
            return mg;
        }

        private void AddQuadProjectZ(MeshGeometry3D mesh, Point3D p0, Point3D p1, double z0)
        {
            Point3D p2 = new Point3D(p0.X, p0.Y, z0);
            Point3D p3 = new Point3D(p1.X, p1.Y, z0);
            AddQuad(mesh, p0, p1, p3, p2);
        }

        private void AddTriangle(MeshGeometry3D mesh, Point3D p0, Point3D p1, Point3D p2)
        {
            int i0 = mesh.Positions.Count;
            mesh.Positions.Add(p0);
            mesh.Positions.Add(p1);
            mesh.Positions.Add(p2);
            Vector3D n = Vector3D.CrossProduct(p1 - p0, p2 - p1);
            n.Normalize();
            mesh.Normals.Add(n);
            mesh.Normals.Add(n);
            mesh.Normals.Add(n);
            mesh.TriangleIndices.Add(i0 + 0); mesh.TriangleIndices.Add(i0 + 1); mesh.TriangleIndices.Add(i0 + 2);
        }

        private void AddMesh(MeshGeometry3D mesh, Point3D p0, Point3D p1, Point3D p2, Point3D p3, int n, int m)
        {
            Vector3D normal = Vector3D.CrossProduct(p1 - p0, p2 - p1);
            normal.Normalize();
            int index0 = mesh.Positions.Count;
            for (int i = 0; i <= n; i++)
            {
                double fi = 1.0 * i / n;
                Point3D pi0 = p0 + (p1 - p0) * fi;
                Point3D pi1 = p3 + (p2 - p3) * fi;

                for (int j = 0; j <= m; j++)
                {
                    double fj = 1.0 * j / m;
                    Point3D p = pi0 + (pi1 - pi0) * fj;

                    mesh.Positions.Add(p);
                    mesh.Normals.Add(normal);
                }
            }
            for (int i = 0; i < n; i++)
            {
                for (int j = 0; j < m; j++)
                {
                    int i0 = index0 + i * (m + 1) + j;
                    int i1 = index0 + (i + 1) * (m + 1) + j;
                    int i2 = index0 + (i + 1) * (m + 1) + j + 1;
                    int i3 = index0 + i * (m + 1) + j + 1;
                    mesh.TriangleIndices.Add(i0); mesh.TriangleIndices.Add(i1); mesh.TriangleIndices.Add(i2);
                    mesh.TriangleIndices.Add(i2); mesh.TriangleIndices.Add(i3); mesh.TriangleIndices.Add(i0);
                }
            }
        }

        private void AddQuad(MeshGeometry3D mesh, Point3D p0, Point3D p1, Point3D p2, Point3D p3)
        {
            int i0 = mesh.Positions.Count;
            mesh.Positions.Add(p0);
            mesh.Positions.Add(p1);
            mesh.Positions.Add(p2);
            mesh.Positions.Add(p3);
            Vector3D n = Vector3D.CrossProduct(p1 - p0, p2 - p1);
            n.Normalize();
            mesh.Normals.Add(n);
            mesh.Normals.Add(n);
            mesh.Normals.Add(n);
            mesh.Normals.Add(n);
            mesh.TriangleIndices.Add(i0 + 0); mesh.TriangleIndices.Add(i0 + 1); mesh.TriangleIndices.Add(i0 + 2);
            mesh.TriangleIndices.Add(i0 + 2); mesh.TriangleIndices.Add(i0 + 3); mesh.TriangleIndices.Add(i0 + 0);
        }

        private void AddQuadProjectZ(MeshGeometry3D mesh, Point3D p0, Point3D p1, Point3D p2, Point3D p3, double z0)
        {
            Point3D q0 = new Point3D(p0.X, p0.Y, z0);
            Point3D q1 = new Point3D(p1.X, p1.Y, z0);
            Point3D q2 = new Point3D(p2.X, p2.Y, z0);
            Point3D q3 = new Point3D(p3.X, p3.Y, z0);
            AddQuad(mesh, q0, q1, q2, q3);
        }


        public int GetKeyNumber(GeometryModel3D model)
        {
            if (!keyToNote.ContainsKey(model))
                return 0;
            return keyToNote[model];
        }
        #endregion
    }
}
